iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0
Vue.js

Vue3.6 的革新:深入理解 Composition API系列 第 10

Day 10: 單向數據流 props / emit / v-model 的關鍵概念

  • 分享至 

  • xImage
  •  

Vue 元件之間的核心溝通方式有三種:

  • props(父 → 子):父層把資料當作 唯讀 來源傳給子層,子層 不可 直接改 prop(單向數據流)。
  • emit(子 → 父):子層告訴父層「發生了什麼事」或「我想改某個值」,由父層決定要不要改資料。
  • v-model(雙向):其實是「props + emit 的語法糖」,標準事件名是 update:modelValue(或使用具名 v-model)。

這三者組合起來,就是 單向數據流 (one-way data flow) 的基礎。

props:父傳子(唯讀),定義、接收與使用


子元件 ⇒ 宣告與定義型別、預設值

<script setup lang="ts">
import { withDefaults, defineProps, toRefs } from 'vue'

const props = withDefaults(defineProps<{
  title: string
  count?: number
  user?: { id: number; name: string }
}>(), {
  title: '預設標題',
  count: 0
})

const { title, count, user } = toRefs(props)
</script>

<template>
  <h3>{{ title }}</h3>
  <p>count: {{ count }}</p>
  <p v-if="user">user: {{ user.name }}</p>
</template>

父元件 ⇒ 傳入 props

引用子元件 <ChildA>

:title="title" → 將父元件的響應式變數 title 傳入子元件的 props.title
:count="count" → 把數字 5 傳下去 (如果父層更新 count,子層會同步更新)。
:user="user" → 傳入一個物件 { id: 1, name: "kuku" } 給子元件。

<script setup lang="ts">
import { ref } from 'vue'
import ChildA from './ChildA.vue'

const title = ref('Hello Props')
const count = ref(5)
const user  = ref({ id: 1, name: 'kuku' })
</script>

<template>
  <ChildA :title="title" :count="count" :user="user" />
</template>

<template> 綁定 ref 時,Vue 會自動幫你解包 .value,所以傳給子元件的其實是 title.valuecount.valueuser.value,而不是 ref 本身。

props 的重點 & 常見坑

  • 單向數據流props 在子元件是 唯讀(shallow readonly)。直接 props.count++ 會在開發模式下警告。

vendor.js:600 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop’s value.
vendor.js:600 [Vue warn]:避免直接修改 prop,因為每次父元件重新渲染時,其值都會被覆寫。建議使用基於 prop 值的資料屬性或計算屬性。

  • 巢狀物件變更props.user.name = 'X' 在技術上改得動(因為淺只讀),但屬於 反模式,應改用 emit 通知父層更新。

  • 解構陷阱const { title } = props 會丟失響應式,請用 toRefs(props) 或直接用 props.title

emit:往父層發事件(回報狀態/請求修改)


子元件 ⇒ 宣告、發送事件

以下是一個組件 ChildB

<script setup lang="ts">
const emit = defineEmits<{
  (e: 'select', id: number): void
  (e: 'save', payload: { id: number; name: string }): void
}>()

function onSelect() { emit('select', 42) }
function onSave()   { emit('save', { id: 1, name: 'New Name' }) }
</script>

<template>
  <button @click="onSelect">Select #42</button>
  <button @click="onSave">Save</button>
</template>

父元件 ⇒ 監聽事件

在父層 @v-on 的縮寫,用來監聽子元件透過 emit 發出的事件。
v-on 和他的縮寫

以下程式碼 @select 代表監聽 ChildB 裡觸發的 "select" 事件,也就是當子元件執行 emit('select', someId) 時,會觸發這裡的箭頭函式。

同理,@save 監聽 ChildB 的 "save" 事件,當子元件呼叫 emit('save', somePayload) 時,會觸發這裡的函式。

<ChildB @select="id => console.log('id:', id)"
        @save="p => console.log('payload:', p)" />

emit 命名建議

  • 事件名採用「動作/過去式」語意,例如:submitselectchange
  • 對於 v-model 對應的事件,使用 update:xxx

v-model(雙向綁定)= props + emit 的語法糖


標準單一 v-modelmodelValue

定義一個 Counter 組件

<script setup lang="ts">
import { computed } from 'vue'

const props = defineProps<{ modelValue: number }>()  // 宣告父層傳進來的 props
const emit  = defineEmits<{ (e: 'update:modelValue', v: number): void }>()  // 宣告子元件可以觸發的事件(emit)

const count = computed({
  // 回傳父層的 props.modelValue,讓畫面能顯示父層傳進來的值
  get: () => props.modelValue, 
  
  // 對 count 賦值時,就呼叫 emit('update:modelValue', v),通知父層更新
  set: v  => emit('update:modelValue', v)  
})
</script>

<template>
  <button @click="count--">-</button>
  <span>{{ count }}</span>
  <button @click="count++">+</button>
</template>

父層就能用:

<Counter v-model="count" />

具名 v-model(多個雙向值)

<Range v-model:min="min" v-model:max="max" />

Vue 3.4 + defineModel 簡寫

Vue 3.4 + defineModel 簡寫 ===「在子層自動生成對應的 props & emit」。

以下是一個計數器,使用 defineModel 自動生成一個 prop:modelValue,型別是 number,預設值為 0,同時自動生成一個 emit:update:modelValue

透過 count 使用時,不用手動寫 getter / setter,因為它已經是雙向響應式的。

<!-- CounterModel.vue -->
<script setup lang="ts">
const count = defineModel<number>({ default: 0 }) // 對應 v-model
</script>

<template>
  <button @click="count--">-</button>
  <span class="mx-2">{{ count }}</span>
  <button @click="count++">+</button>
</template>

接下來一樣使用 defineModel 做多個 v-model

  • defineModel<number>('min', { default: 0 }) -> 自動生成 props.min,型別 number,預設值 0,以及 emit('update:min', v)

  • defineModel<number>('max', { default: 100 }) -> 也會自動生成 props.max,型別 number,預設值 100,emit('update:max', v)

  • input 中透過 v-model:number 語法糖,雙向綁定 min

<!-- RangeModel.vue -->
<script setup lang="ts">
const min = defineModel<number>('min', { default: 0 })
const max = defineModel<number>('max', { default: 100 })
</script>

<template>
  <input type="number" v-model.number="min" />
  <input type="number" v-model.number="max" />
</template>

以上兩個組件就可以在父層就可以做雙向綁定

<script setup lang="ts">
import CounterModel from './CounterModel.vue'
import RangeModel from './RangeModel.vue'

const myCount = ref(5)
const myMin = ref(10)
const myMax = ref(90)
</script>

<template>
  <CounterModel v-model="myCount" />
  <p>父層 count: {{ myCount }}</p>

  <RangeModel v-model:min="myMin" v-model:max="myMax" />
  <p>父層範圍: {{ myMin }} - {{ myMax }}</p>
</template>

單向數據流、雙向綁定與響應式更新(運作原理與實務)


父狀態 → (props) → 子元件(唯讀 readonly)
          ↑
        (emit)

父層是單一真相來源。
子層若要暫存修改(例如表單),要先做本地副本,最後再 emit 更新。

// ChildEdit.vue(子)——本地可編輯副本
import { ref, watch } from 'vue'
const props = defineProps<{ modelValue: string }>()
const emit  = defineEmits<{ (e:'update:modelValue', v:string): void }>()

const draft = ref(props.modelValue)
// 父層更新時同步本地值
watch(() => props.modelValue, v => { draft.value = v })

function save() {
  emit('update:modelValue', draft.value)
}

整合範例 - 搜尋框


<script setup lang="ts">
import { computed } from 'vue'

const props = defineProps<{ modelValue: string }>()
const emit  = defineEmits<{
  (e:'update:modelValue', v:string): void
  (e:'submit', v:string): void
}>()

const keyword = computed({
  get: () => props.modelValue,
  set: v  => emit('update:modelValue', v)
})

function onSubmit() { emit('submit', keyword.value.trim()) }
</script>

<template>
  <div>
    <input v-model="keyword" placeholder="輸入關鍵字…" />
    <button @click="onSubmit">搜尋</button>
  </div>
</template>

父層:

<SearchInput v-model="kw" @submit="v => console.log('Search:', v)" />

小結


  • props:父層要給子層顯示或計算所需的資料、或調整子層行為的設定值(如 disabledsizecolumns)。

  • emit:子層 通知父層事件(如 submit/select),或提出 我想改某個值 的請求(搭配 update:*v-model)。

    props vs emit

  • v-model:當子層看起來像「一個可控表單或控件」時(input、select、dialog 的 open 等),用 v-model 最直覺,之後我們會更一步說明 v-model 在 Vue3.5 的進化。

  • 單向數據流 保證狀態一致性,父層永遠是唯一來源。

參考資料


  1. Vue.sj - props
  2. Vue.js - 組件事件
  3. Vue.js - 組件 v-model

上一篇
Day 9: watchEffect 的介紹
下一篇
Day 11: 跨元件的 provide / inject
系列文
Vue3.6 的革新:深入理解 Composition API12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言